/**
 * Copyright (c) 2012 Nokia Corporation.
 */

package com.nokia.example.productcatalog;

import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;

import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Choice;
import javax.microedition.lcdui.ChoiceGroup;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Gauge;
import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.ItemCommandListener;
import javax.microedition.lcdui.ItemStateListener;
import javax.microedition.lcdui.Spacer;
import javax.microedition.lcdui.StringItem;
import javax.microedition.midlet.MIDlet;

import com.nokia.mid.payment.IAPClientPaymentException;
import com.nokia.mid.payment.IAPClientPaymentListener;
import com.nokia.mid.payment.IAPClientPaymentManager;
import com.nokia.mid.payment.IAPClientProductData;
import com.nokia.mid.payment.IAPClientUserAndDeviceData;

/**
 * This class provides the very basic implementation of Nokia In-application
 * Purchase feature for Series 40. This reference code will guide you in writing
 * and integration your IAP solution. You may need to modify the code to make it
 * meet your application requirements.
 * 
 * If you are using production In-app server, please check the proxy setting 
 * of the emulator in the SDK preferences.
 */
public class ProductCatalogForm
	extends Form 
    implements IAPClientPaymentListener,
    		   CommandListener,
    		   ItemStateListener
{
    // Constants
    private static final String DRM_PATH = "drm/data/resourceid_";
    private static final String PRODUCT_DATA_FILENAME = "data.txt";
    private static final char PATH_SEPARATOR = '/';
    private static final String RECORD_STORE_NAME = "productCatalogRms";
    private static final int SPINNER_ITEM_NUM = 1;

    // Strings
    private static final String STR_BUY = "Buy";
    private static final String STR_UNLOCKED_CONTENT = "Unlocked content";
    private static final String STR_ITEMS_TO_BUY = "Items to buy";
    private static final String STR_ITEMS_BOUGHT = "Items you have bought";
    private static final String STR_PLEASE_WAIT = "Please wait...";
    
    // Members
    private final MIDlet midlet;
    private final String[] productIds;
    private final Command buyCommand;
    private final Command viewCommand;
    private final ChoiceGroup itemsBoughtGroup;
    private final Gauge gauge;
    private final Spacer spacer = new Spacer(15, 1);
    private final BasicRMS rms = new BasicRMS();
    private ChoiceGroup itemsToBuyGroup = null;
    private ProductData[] productDataList = null;
    private ItemIndex[] boughtItemIndexes = null;
    private StringItem buyButton = null;
    private IAPClientPaymentManager iapClient = null;
    private int itemToBuyIndex = ItemIndex.INVALID_INDEX;
    private boolean isBusy = false;

    /**
     * Constructor.
     * @param midlet
     * @param title
     * @param productIds
     */
    public ProductCatalogForm(final MIDlet midlet,
    						  String title,
    						  String[] productIds)
    {
        super(title);
        this.midlet = midlet;
        this.productIds = productIds;
        buyCommand = new Command(STR_BUY, Command.ITEM, 0);
        viewCommand = new Command(STR_UNLOCKED_CONTENT, Command.ITEM, 0);
        itemsBoughtGroup = new ChoiceGroup(STR_ITEMS_BOUGHT, Choice.MULTIPLE);
        
        itemsToBuyGroup =
        		new ChoiceGroup(STR_ITEMS_TO_BUY, Main.isNonTouch() ?
        				Choice.MULTIPLE : Choice.EXCLUSIVE);
        
        gauge = new Gauge(STR_PLEASE_WAIT, false, Gauge.INDEFINITE,
        				  Gauge.CONTINUOUS_RUNNING);
        
        append(itemsToBuyGroup);
        createBuyButton();
        append(spacer);
        append(itemsBoughtGroup);
        
        itemsToBuyGroup.addCommand(buyCommand);
        itemsBoughtGroup.addCommand(viewCommand);
        
        setCommandListener(this);
        setItemStateListener(this);
    }
    
    /**
     * From CommandListener.
     */
    public void commandAction(Command command, Displayable displayable) {
        if (command == buyCommand) {
        	purchaseSelectedItem();
        }
        else if (command == viewCommand) {
            showBoughtItemDetails();
        }
    }
    
    // ============================= Methods from IAPClientPaymentListener --->

    public void productDataListReceived(int status,
    									IAPClientProductData[] list)
    {
        setBusy(false);
        productDataList = new ProductData[list.length];
        boughtItemIndexes = new ItemIndex[list.length];
        
        for (int i = 0; i < productDataList.length; i++) {
            productDataList[i] = new ProductData();
            productDataList[i].productId = list[i].getProductId();
            productDataList[i].title = list[i].getTitle();
            productDataList[i].shortDescription = list[i].getShortDescription();
            productDataList[i].longDescription = list[i].getLongDescription();
            productDataList[i].price = list[i].getPrice();
            productDataList[i].drmProtection = list[i].getDrmProtection();
            productDataList[i].listIndex = i;
            boughtItemIndexes[i] = new ItemIndex();
        }
        
        deserialiseProducts(productDataList);
        
        for (int i = 0; i < productDataList.length; i++) {
            if (!productDataList[i].isUnlocked) {
                itemsToBuyGroup.append(productDataList[i].shortDescription
                		+ ", price: " + productDataList[i].price, null);
            }
        }
        
        itemsToBuyGroup.setFitPolicy(Choice.TEXT_WRAP_ON);
        
        if (Main.isNonTouch()) {
            itemsToBuyGroup.setSelectedFlags(new boolean[productDataList.length]);
        }
        
        updateGroupTitles();
    }

    public void purchaseCompleted(int status, String purchaseTicket) {
        setBusy(false);
        
        if (status != IAPClientPaymentListener.OK
        		&& status != IAPClientPaymentListener.RESTORABLE)
        {
            return;
        }
        
        ProductData product = getItemToBuy(itemToBuyIndex);
        product.errorState = status;
        product.ticket = purchaseTicket;
        product.isUnlocked = true;
        
        itemsBoughtGroup.append(product.longDescription + " unlocked", null);
        
        // Note: If itemToBuyIndex == 0, itemsToBuyGroup.delete(itemToBuyIndex)
        // results in out-of-bounds exception on Asha touch and type phones but
        // works on full touch devices.
        //
        // Workaround below: Use itemsToBuyGroup.deleteAll() instead
        String [] productArray = new String [itemsToBuyGroup.size()];
        
        if (productArray.length > 1) {
        	for (int i = 0; i < itemsToBuyGroup.size(); i++) {
        		productArray[i] = itemsToBuyGroup.getString(i); 
        	}
        }
        
        itemsToBuyGroup.deleteAll();

        if (productArray.length > 1) {
        	for (int i = 0; i < productArray.length; i++) {
        		if (i == itemToBuyIndex) {
        			continue;
        		}
        		
        		itemsToBuyGroup.append(productArray[i], null); 
        	}
        }
        
        // End of workaround
        
        itemToBuyIndex = ItemIndex.INVALID_INDEX;
        
        appendBoughtItem(new ItemIndex(itemsBoughtGroup.size() - 1,
        				 product.listIndex));
        
        updateGroupTitles();
        
        serialiseProducts(product.productId);
    }

    public void productDataReceived(int status, IAPClientProductData productData) { /* Not used */ }
    public void restorationCompleted(int status, String purchaseTicket) { /* Not used */ }
    public void restorableProductsReceived(int status, IAPClientProductData[] productDataList) { /* Not used */ }
    public void userAndDeviceDataReceived(int status, IAPClientUserAndDeviceData ud) { /* Not used */ }

    // <--- methods from IAPClientPaymentListener =============================

    /**
     * From ItemStateListener.
     */
    public void itemStateChanged(Item item) {
        if (isBusy) {
            return;
        }
        
        if (item != null && item.equals(itemsToBuyGroup) && Main.isNonTouch()) {
            purchaseSelectedItem();
        }
        
        if (item !=null && item.equals(itemsBoughtGroup)) {
            showBoughtItemDetails();
        }
    }
    
    /**
     * Retrieves the products using the In-application Purchase API.
     */
    public void init() {
        if (iapClient == null) {
            try {
				iapClient = IAPClientPaymentManager.getIAPClientPaymentManager();
			}
            catch (IAPClientPaymentException e) {
	        	showErrorMessage(
	            		"Fatal error: IAPClientPaymentManager::getIAPClientPaymentManager() failed: "
	            		+ e.toString());
				e.printStackTrace();
			}
            
            IAPClientPaymentManager.setIAPClientPaymentListener(this);
        }
        
        int errorCode = iapClient.getProductData(productIds);
        
        if (errorCode == IAPClientPaymentManager.SUCCESS) {
        	setBusy(true);
        }
        else {
        	showErrorMessage(
            		"Fatal error: IAPClientPaymentManager::getProductData() failed with error code "
            		+ errorCode);
        }
    }
    
    /** 
     * @param busy If true, will show the gauge with text "Please wait".
     * 			   If false, will display the buy button (for touch devices).
     */
    private void setBusy(boolean busy) {
    	if (busy) {
    		if (!Main.isNonTouch()) {
    			delete(SPINNER_ITEM_NUM);
    		}
    		
    		insert(SPINNER_ITEM_NUM, gauge);
    	}
    	else {
            delete(SPINNER_ITEM_NUM);
            
            if (!Main.isNonTouch()) {
                insert(SPINNER_ITEM_NUM, buyButton);
            }
    	}
    	
        isBusy = busy;
    }
    
    /**
     * Resolves the product data instance, associated with an item that is for
     * sell but not yet bought, based on the given index.
     * @param index The index of the item.
     * @return The product data associated with the item or null if not found.
     */
    private ProductData getItemToBuy(int index){
        ProductData result = null;
        
        for (int i = 0, sought = index; i < productDataList.length; i++) {
            result = productDataList[i];
            
            if (!result.isUnlocked) {
                sought--;
            }
                        
            if (sought == -1) {
                return result;
            }
        }
        
        return null;
    }

    /**
     * Appends the item with the given index into the list of bought items.
     * @param boughtItemIndex The index of the item.
     */
    private void appendBoughtItem(ItemIndex boughtItemIndex) {
        for (int i = 0; i < boughtItemIndexes.length; i++) {
            if (boughtItemIndexes[i].groupIndex() == ItemIndex.INVALID_INDEX) {
                boughtItemIndexes[i] = boughtItemIndex;
                break;
            }
        }
    }

    /**
    /**
     * Resolves the product data instance, associated with a bought item, based
     * on the given index.
     * @param groupIndex The index of the item.
     * @return The product data associated with the item or null if not found.
     */
    private ProductData getBoughtItem(int groupIndex) {
        for (int i = 0; i < boughtItemIndexes.length; i++) {
            if (boughtItemIndexes[i].groupIndex() == groupIndex) {
                return productDataList[boughtItemIndexes[i].listIndex()];
            }
        }
        
        return null;
    }

    /**
     * Returns the index of the selected item in the given group.
     * @param itemsGroup The item group.
     * @return The index of the selected item or ItemIndex.INVALID_INDEX if not found.
     */
    private int getSelectedIndex(ChoiceGroup itemsGroup) {
        boolean[] selectedIndexes = new boolean[itemsGroup.size()];
        itemsGroup.getSelectedFlags(selectedIndexes);
        
        for (int i = 0; i < selectedIndexes.length; i++) {
            if (selectedIndexes[i]) {
                return i;
            }
        }
        
        return ItemIndex.INVALID_INDEX;
    }

    /**
     * Tries to purchase the item currently selected. If successful, this
     * launches the In-application Purchase flow provided by the framework
     * itself.
     */
    private void purchaseSelectedItem() {
        if (!Main.isNonTouch()) {
            itemToBuyIndex = itemsToBuyGroup.getSelectedIndex();
        }
        else {
            itemToBuyIndex = getSelectedIndex(itemsToBuyGroup);
            
            // Reset the selection
            itemsToBuyGroup.setSelectedFlags(new boolean[itemsToBuyGroup.size()]); 
        }
        
        int errorCode =
        		iapClient.purchaseProduct(
        				getItemToBuy(itemToBuyIndex).productId, 
                        IAPClientPaymentManager.FORCED_AUTOMATIC_RESTORATION);

        if (errorCode == IAPClientPaymentManager.SUCCESS) {
            setBusy(true);
        }
        else {
            itemToBuyIndex = ItemIndex.INVALID_INDEX;
            showErrorMessage("IAPClientPaymentManager::purchaseProduct() failed with error code "
            		+ errorCode);
        }
    }

    /**
     * Displays the details of the bought item.
     */
    private void showBoughtItemDetails() {
        int index = getSelectedIndex(itemsBoughtGroup);
        
        // Reset the selection
        itemsBoughtGroup.setSelectedFlags(new boolean[itemsBoughtGroup.size()]);
        
        ProductData product = getBoughtItem(index);
        StringBuffer info = new StringBuffer();
        
        try {
            InputStream input =
            		IAPClientPaymentManager.getIAPClientPaymentManager().
            			getDRMResourceAsStream(DRM_PATH + product.productId
            					+ PATH_SEPARATOR + PRODUCT_DATA_FILENAME);
            
            int data = input.read();
            
            while (data != -1) {
            	info.append((char)data);
            	data = input.read();
            }
            
            input.close();
        }
        catch (IOException ex) {
            info.append("Failed to load DRM file: ");
            info.append(ex.getMessage());
        }
        catch (IAPClientPaymentException ex) {
            info.append(ex.toString());
        }
        finally {
            Alert alert = new Alert("", info.toString(), null, AlertType.CONFIRMATION);
            alert.setTimeout(Alert.FOREVER);
            Display.getDisplay(midlet).setCurrent(alert);
        }
    }

    /**
     * Updates the titles of the groups based on the current number of items
     * per group.
     */
    private void updateGroupTitles() {
        itemsBoughtGroup.setLabel(STR_ITEMS_BOUGHT + " (" + itemsBoughtGroup.size() + ")");
        itemsToBuyGroup.setLabel(STR_ITEMS_TO_BUY + " (" + itemsToBuyGroup.size() + ")");
    }

    /**
     * Creates the buy button.
     */
    private void createBuyButton() {
        if (Main.isNonTouch()) {
            return;
        }
        
        buyButton = new StringItem(null, STR_BUY, Item.BUTTON);
        buyButton.setDefaultCommand(buyCommand);
        
        buyButton.setItemCommandListener(new ItemCommandListener() {
            public void commandAction(Command command, Item item) {
                purchaseSelectedItem();
            }
        });
        
        insert(SPINNER_ITEM_NUM, buyButton);
    }
    
    /**
     * Displays an alert with the given error message.
     * @param errorMessage The error message to show.
     */
    private void showErrorMessage(final String errorMessage) {
    	Alert alert = new Alert("Error", errorMessage, null, AlertType.ERROR);
    	alert.setTimeout(5000);
    	Display.getDisplay(midlet).setCurrent(alert);
    }
    
	/**
	 * Appends the given ID of a bought item into the record store.
	 * @param productId The ID of the bought item.
	 */
    private void serialiseProducts(String productId) {
        rms.open(RECORD_STORE_NAME);
        rms.set(productId.getBytes());
        rms.close();
    }

    /**
     * Retrieves the bought items from the record store.
     * @param productDataList The list where the bought items are added to.
     */
    private void deserialiseProducts(ProductData[] productDataList) {
        rms.open(RECORD_STORE_NAME);
        Vector productArray = rms.get();
        
        if (!productArray.isEmpty()) {
            for (int i = 0; i < productDataList.length; i++) {
                ProductData product = productDataList[i];
                
                if (productArray.contains(product.productId)) {
                    product.errorState = IAPClientPaymentListener.OK;
                    product.ticket = "from RMS";
                    product.isUnlocked = true;
                    itemsBoughtGroup.append(product.longDescription + " unlocked", null);
                    appendBoughtItem(new ItemIndex(itemsBoughtGroup.size() - 1, product.listIndex));
                }
            }
        }
        
        rms.close();
    }
}
